home *** CD-ROM | disk | FTP | other *** search
/ Enter 2006 September / Enter 09 2006.iso / Internet / SpamExperts Home 1.1 / SpamExperts Home.exe / lib / spamexperts.modules / spamexperts / pop3.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2006-07-14  |  33.2 KB  |  1,150 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.4)
  3.  
  4. '''POP3 Classes
  5.  
  6. This module contains classes used for interacting with POP3 clients or
  7. servers.  The module currently includes:
  8.  
  9. POP3
  10.     This class is identical to poplib.POP3, but redirects debugging output
  11.     to a specified file.  Some poplib.POP3 debugging output is also
  12.     suppressed.
  13.  
  14. SEPOP3Server
  15.     This class is a simple POP3 server that acts as a proxy between a POP3
  16.     client and a real POP3 server.  All RFC 1725 commands are implemented,
  17.     but no others (e.g. PIPELINING, STLS) apart from CAPA.  The mail on the
  18.     server is retrieved from the real POP3 server, once per instantiation
  19.     of the class.  Mail classified as spam is blocked (but may be released
  20.     later elsewhere), and other mail is made available to connecting
  21.     clients.
  22. '''
  23. import os
  24. import re
  25. import sys
  26. import md5
  27. import copy
  28. import time
  29. import email
  30. import poplib
  31. import thread
  32. import socket
  33. import string
  34. import asynchat
  35. import traceback
  36. import threading
  37. import email.Header as email
  38.  
  39. try:
  40.     import cStringIO as StringIO
  41. except ImportError:
  42.     import StringIO
  43.  
  44. from spambayes import Dibbler
  45. from spambayes.storage import NO_TRAINING_FLAG
  46. from spambayes.message import insert_exception_header
  47. from spamexperts import Options
  48. verbose = Options.options[('globals', 'verbose')]
  49. Options.options[('globals', 'verbose')] = False
  50. from spambayes.scripts.sb_server import BayesProxy, POP3ProxyBase
  51. Options.options[('globals', 'verbose')] = verbose
  52. del verbose
  53. from spamexperts import message, ProxyClassifier
  54. from se_config import spamexpertsConfig as configuration
  55. from spamexperts.OptionsClass import BLOCKED, DELAYED, REMOVED
  56. from spamexperts.OptionsClass import IS_HAM, IS_SPAM, IS_UNSURE
  57.  
  58. class SEPOP3Proxy(BayesProxy, ProxyClassifier.ProxyClassifier):
  59.     """Proxies between an email client and a POP3 server, inserting
  60.     judgement headers.  It acts on the following POP3 commands:
  61.  
  62.      o STAT:
  63.         o Adds the size of all the judgement headers to the maildrop
  64.           size.
  65.  
  66.      o LIST:
  67.         o With no message number: adds the size of an judgement header
  68.           to the message size for each message in the scan listing.
  69.         o With a message number: adds the size of an judgement header
  70.           to the message size.
  71.  
  72.      o RETR:
  73.         o Adds the judgement header based on the raw headers and body
  74.           of the message.
  75.  
  76.      o TOP:
  77.         o Adds the judgement header based on the raw headers and as
  78.           much of the body as the TOP command retrieves.  This can
  79.           mean that the header might have a different value for
  80.           different calls to TOP, or for calls to TOP vs. calls to
  81.           RETR.  I'm assuming that the email client will either not
  82.           make multiple calls, or will cope with the headers being
  83.           different.
  84.  
  85.      o USER:
  86.         o Does no processing based on the USER command itself, but
  87.           expires any old messages in the three caches.
  88.  
  89.     This class does not provide any access to messages that have
  90.     been delayed or blocked by the blocking POP3 proxy.
  91.     """
  92.     
  93.     def __init__(self, clientSocket, unused, serverName, serverPort, state, ssl = False):
  94.         self.skip_logging = False
  95.         self.serverName = serverName
  96.         self.serverPort = serverPort
  97.         ProxyClassifier.ProxyClassifier.__init__(self)
  98.         BayesProxy.__init__(self, clientSocket, serverName, serverPort, ssl)
  99.         self.state = state
  100.         self.state.proxies.append(self)
  101.         self.state.model_notifier.SetBeginUpdating()
  102.         self.past_headers = False
  103.         self.past_ok = False
  104.         self.last_character = None
  105.  
  106.     
  107.     def onResponse(self):
  108.         if self.serverName in Options.options[('pop3proxy', 'ignore_uidl_for_servers')]:
  109.             for unsupported in [
  110.                 'UIDL']:
  111.                 unsupportedLine = '(?im)^%s[^\\n]*\\n' % (unsupported,)
  112.                 self.response = re.sub(unsupportedLine, '', self.response)
  113.             
  114.         
  115.         BayesProxy.onResponse(self)
  116.  
  117.     
  118.     def send(self, data):
  119.         '''Logs the data to the log file.'''
  120.         if self.skip_logging:
  121.             if '\n' in data:
  122.                 if data == '\n':
  123.                     lines = ('',)
  124.                 else:
  125.                     lines = data.split('\n')
  126.                 for line in lines:
  127.                     if not line == '':
  128.                         pass
  129.                     is_newline = line == '\r'
  130.                     if not (self.past_ok):
  131.                         if is_newline or line.startswith('+OK'):
  132.                             self.past_ok = True
  133.                         elif self.past_ok and is_newline and self.last_character == '':
  134.                             self.past_headers = True
  135.                         
  136.                     if self.past_headers:
  137.                         if (line.strip() == '.' or is_newline) and self.last_character == '.':
  138.                             self.skip_logging = False
  139.                         
  140.                     if self.skip_logging:
  141.                         self.state.logFile.write('...CONTENTS...\r\n')
  142.                     else:
  143.                         self.state.logFile.write(time.asctime() + ':' + line)
  144.                     self.last_character = ''
  145.                 
  146.             elif data != '\r':
  147.                 self.state.logFile.write('...CONTENTS...\r\n')
  148.                 self.last_character = data[-1]
  149.             
  150.         else:
  151.             self.state.logFile.write(time.asctime() + ':' + data)
  152.         self.state.logFile.flush()
  153.         
  154.         try:
  155.             return POP3ProxyBase.send(self, data)
  156.         except socket.error:
  157.             self.close()
  158.  
  159.         return 0
  160.  
  161.     
  162.     def recv(self, size):
  163.         '''Logs the data to the log file.'''
  164.         
  165.         try:
  166.             data = POP3ProxyBase.recv(self, size)
  167.         except socket.error:
  168.             self.close()
  169.             return ''
  170.  
  171.         if data.lower().startswith('user'):
  172.             log_data = 'USER XXXXXXXX\r\n'
  173.         elif data.lower().startswith('pass'):
  174.             log_data = 'PASS XXXXXXXX\r\n'
  175.         elif not (self.skip_logging) or data.lower().startswith('retr') or data.lower().startswith('top'):
  176.             self.skip_logging = True
  177.             self.past_headers = False
  178.             self.past_ok = False
  179.             self.last_character = None
  180.             log_data = data
  181.         else:
  182.             log_data = data
  183.         self.state.logFile.write(time.asctime() + ':' + log_data)
  184.         self.state.logFile.flush()
  185.         return data
  186.  
  187.     
  188.     def close(self):
  189.         if not self.isClosed:
  190.             self.isClosed = True
  191.             self.state.activeSessions -= 1
  192.             POP3ProxyBase.close(self)
  193.             self.serverSocket.close()
  194.             self.state.proxies.remove(self)
  195.             self.state.model_notifier.SetEndUpdating()
  196.         
  197.  
  198.     
  199.     def _do_classification(self, msg):
  200.         
  201.         try:
  202.             self.classification_done = self.classify_message(msg)
  203.         except:
  204.             stream = StringIO.StringIO()
  205.             traceback.print_exc(None, stream)
  206.             details = stream.getvalue()
  207.             self.classification_done = Exception(details)
  208.  
  209.  
  210.     
  211.     def onRetr(self, command, args, response):
  212.         if re.search('\\n\\r?\\n', response):
  213.             terminatingDotPresent = response[-4:] == '\n.\r\n'
  214.             if terminatingDotPresent:
  215.                 response = response[:-3]
  216.             
  217.             (ok, messageText) = response.split('\n', 1)
  218.             
  219.             try:
  220.                 msg = email.message_from_string(messageText, _class = message.SEHeaderMessage)
  221.                 msg.setId(self.state.getNewMessageName())
  222.                 self.classification_done = False
  223.                 thread.start_new_thread(self._do_classification, (msg,))
  224.                 score = None
  225.                 for already_sent, c in enumerate(ok + '\n' + msg.as_string()[:60]):
  226.                     self.send(c)
  227.                     if self.classification_done:
  228.                         if isinstance(self.classification_done, Exception):
  229.                             raise self.classification_done
  230.                         
  231.                         (score, classification, clues) = self.classification_done
  232.                         break
  233.                     
  234.                     time.sleep(1)
  235.                 
  236.                 if score is None:
  237.                     while True:
  238.                         if self.classification_done:
  239.                             (score, classification, clues) = self.classification_done
  240.                             break
  241.                         
  242.                         time.sleep(1)
  243.                 
  244.                 if Options.options[('globals', 'verbose')]:
  245.                     print >>sys.stderr, 'Classification: %s' % (classification,)
  246.                 
  247.                 if classification == IS_HAM:
  248.                     classification = Options.options[('Headers', 'header_ham_string')]
  249.                 elif classification == IS_SPAM:
  250.                     classification = Options.options[('Headers', 'header_spam_string')]
  251.                 elif classification == IS_UNSURE:
  252.                     classification == Options.options[('Headers', 'header_unsure_string')]
  253.                 
  254.                 if command == 'RETR' and command == 'TOP' and len(args) == 2:
  255.                     pass
  256.                 correct_command = args[1] == '99999999'
  257.                 if classification == Options.options[('Headers', 'header_ham_string')] and Options.options[('Storage', 'no_cache_bulk_ham')]:
  258.                     pass
  259.                 isSuppressedBulkHam = msg.get('precedence') in [
  260.                     'bulk',
  261.                     'list']
  262.                 size_limit = Options.options[('Storage', 'no_cache_large_messages')]
  263.                 if size_limit > 0:
  264.                     pass
  265.                 isTooBig = len(messageText) > size_limit
  266.                 if correct_command and Options.options[('Storage', 'cache_messages')] and not isSuppressedBulkHam and not isTooBig:
  267.                     msg.RememberClassification(classification)
  268.                     msg.addHeaders(prob = score, clues = clues)
  269.                     if classification == Options.options[('Headers', 'header_spam_string')]:
  270.                         self.state.numSpams += 1
  271.                         corpus = self.state.spamCorpus
  272.                     elif classification == Options.options[('Headers', 'header_unsure_string')]:
  273.                         corpus = self.state.unsureCorpus
  274.                     else:
  275.                         self.state.numHams += 1
  276.                         corpus = self.state.hamCorpus
  277.                     msg = corpus.makeMessage(msg.getId(), msg.as_string())
  278.                     corpus.addMessage(msg, observer_flags = NO_TRAINING_FLAG)
  279.                 
  280.                 headers = []
  281.                 for name, value in msg.items():
  282.                     header = '%s: %s' % (name, value)
  283.                     headers.append(re.sub('\\r?\\n', '\r\n', header))
  284.                 
  285.                 body = re.split('\\n\\r?\\n', messageText, 1)[1]
  286.                 messageText = '\r\n'.join(headers) + '\r\n\r\n' + body
  287.                 retval = ok + '\n' + messageText
  288.                 if terminatingDotPresent:
  289.                     retval += '.\r\n'
  290.                 
  291.                 retval = retval[already_sent + 1:]
  292.             except:
  293.                 (messageText, details) = insert_exception_header(messageText)
  294.                 print >>sys.stderr, details
  295.                 retval = ok + '\n' + messageText
  296.                 if terminatingDotPresent:
  297.                     retval += '.\r\n'
  298.                 
  299.  
  300.             return retval
  301.         else:
  302.             self.skip_logging = False
  303.             return response
  304.  
  305.     
  306.     def onUser(self, unused, unused2, response):
  307.         '''Spins off two separate threads that expires any old messages
  308.         in the caches, but does not do any processing of the USER command
  309.         itself.'''
  310.         thread.start_new_thread(self.state.spamCorpus.removeExpiredMessages, ())
  311.         thread.start_new_thread(self.state.hamCorpus.removeExpiredMessages, ())
  312.         return response
  313.  
  314.  
  315.  
  316. class POP3(poplib.POP3):
  317.     '''Just like the parent class, but redirects the debugging output.'''
  318.     
  319.     def __init__(self, logfile, host, port = poplib.POP3_PORT):
  320.         self.logfile = logfile
  321.         self.descriptor = '%s:%s' % (host, port)
  322.         poplib.POP3.__init__(self, host, port)
  323.  
  324.     
  325.     def _putline(self, line):
  326.         self.sock.sendall('%s%s' % (line, poplib.CRLF))
  327.  
  328.     
  329.     def _putcmd(self, line):
  330.         if self._debugging:
  331.             if line.lower().startswith('user'):
  332.                 repr_line = 'USER XXXXXXXX'
  333.             elif line.lower().startswith('pass'):
  334.                 repr_line = 'PASS XXXXXXXX'
  335.             else:
  336.                 repr_line = repr(line)
  337.             self.logfile.write('%s: %s CMD: %s\r\n' % (time.asctime(), self.descriptor, repr_line))
  338.         
  339.         self._putline(line)
  340.  
  341.     
  342.     def _getline(self):
  343.         line = self.file.readline()
  344.         if self._debugging > 1:
  345.             self.logfile.write('%s: %s GET: %s\r\n' % (time.asctime(), self.descriptor, repr(line)))
  346.         
  347.         if not line:
  348.             raise poplib.error_proto('-ERR EOF')
  349.         
  350.         octets = len(line)
  351.         if line[-2:] == poplib.CRLF:
  352.             return (line[:-2], octets)
  353.         
  354.         if line[0] == poplib.CR:
  355.             return (line[1:-1], octets)
  356.         
  357.         return (line[:-1], octets)
  358.  
  359.     
  360.     def _getresp(self):
  361.         (resp, o) = self._getline()
  362.         c = resp[:1]
  363.         if c != '+':
  364.             raise poplib.error_proto(resp)
  365.         
  366.         return resp
  367.  
  368.     
  369.     def stat(self):
  370.         retval = self._shortcmd('STAT')
  371.         rets = retval.split()
  372.         numMessages = int(rets[1])
  373.         sizeMessages = int(rets[2])
  374.         return (numMessages, sizeMessages)
  375.  
  376.  
  377.  
  378. class POP3_SSL(poplib.POP3_SSL):
  379.     '''Just like the parent class, but redirects the debugging output.'''
  380.     
  381.     def __init__(self, logfile, host, port = poplib.POP3_SSL_PORT, keyfile = None, certfile = None):
  382.         self.logfile = logfile
  383.         self.descriptor = '%s:%s' % (host, port)
  384.         poplib.POP3_SSL.__init__(self, host, port, keyfile, certfile)
  385.  
  386.     
  387.     def _putcmd(self, line):
  388.         if self._debugging:
  389.             if line.lower().startswith('user'):
  390.                 repr_line = 'USER XXXXXXXX'
  391.             elif line.lower().startswith('pass'):
  392.                 repr_line = 'PASS XXXXXXXX'
  393.             else:
  394.                 repr_line = repr(line)
  395.             self.logfile.write('%s: %s CMD: %s\r\n' % (time.asctime(), self.descriptor, repr_line))
  396.         
  397.         self._putline(line)
  398.  
  399.     
  400.     def _getline(self):
  401.         debug = self._debugging
  402.         self._debugging = 0
  403.         (line, octets) = poplib.POP3_SSL._getline(self)
  404.         self._debugging = debug
  405.         if self._debugging > 1:
  406.             self.logfile.write('%s: %s GET: %s\r\n' % (time.asctime(), self.descriptor, repr(line)))
  407.         
  408.         return (line, octets)
  409.  
  410.     
  411.     def _putline(self, line):
  412.         debug = self._debugging
  413.         self._debugging = 0
  414.         poplib.POP3_SSL._putline(self, line)
  415.         self._debugging = debug
  416.  
  417.     
  418.     def _getresp(self):
  419.         (resp, o) = self._getline()
  420.         c = resp[:1]
  421.         if c != '+':
  422.             raise poplib.error_proto(resp)
  423.         
  424.         return resp
  425.  
  426.  
  427.  
  428. class POPRetriever(object):
  429.     
  430.     class _dummy_msg(object):
  431.         '''Dummy message class used to check if a message is in the
  432.         message info database.'''
  433.         
  434.         def __init__(self, key):
  435.             
  436.             self.getDBKey = lambda : key
  437.             self.stored_attributes = [
  438.                 'c',
  439.                 't',
  440.                 'block_state',
  441.                 'account',
  442.                 'date_modified',
  443.                 'internaldate',
  444.                 'flags',
  445.                 'folder_name',
  446.                 'uid']
  447.             for att in self.stored_attributes:
  448.                 setattr(self, att, None)
  449.             
  450.  
  451.  
  452.     
  453.     def close(self):
  454.         
  455.         try:
  456.             self.state.proxies.remove(self)
  457.         except ValueError:
  458.             print >>sys.stderr, 'Connection missing from proxies list.'
  459.  
  460.         
  461.         try:
  462.             self.state.open_remote_connections.remove(self.current_account)
  463.         except ValueError:
  464.             print >>sys.stderr, 'Connection missing from open connections list.'
  465.  
  466.  
  467.     
  468.     def _normalise_name(self, name):
  469.         return [](_[1])
  470.  
  471.     
  472.     def _normalise_uid(self, uid):
  473.         norm_current_account = self._normalise_name(self.current_account)
  474.         norm_uid = self._normalise_name(uid)
  475.         return '%s_%s' % (norm_current_account, norm_uid.lower())
  476.  
  477.     terminated = False
  478.     
  479.     def retrieveMessages(self):
  480.         '''Collect any new mail from the remote server.'''
  481.         p = self.remote_server
  482.         
  483.         try:
  484.             response = p.uidl()
  485.         except poplib.error_proto:
  486.             pass
  487.  
  488.         
  489.         try:
  490.             (response, msg_list, unused) = p.list()
  491.         except poplib.error_proto:
  492.             None if self.serverName not in Options.options[('pop3proxy', 'ignore_uidl_for_servers')] else []
  493.             e = None if self.serverName not in Options.options[('pop3proxy', 'ignore_uidl_for_servers')] else []
  494.             print >>sys.stderr, 'Cannot list messages:', str(e)
  495.             return None
  496.         except:
  497.             None if self.serverName not in Options.options[('pop3proxy', 'ignore_uidl_for_servers')] else []
  498.  
  499.         for msg in msg_list:
  500.             if self.terminated:
  501.                 break
  502.             
  503.             (msg_id, unused) = msg.split()
  504.             uid = None
  505.             
  506.             try:
  507.                 response = p.uidl(msg_id)
  508.             except poplib.error_proto:
  509.                 e = None
  510.  
  511.             (unused, unused, raw_uid) = response.split(' ', 3)
  512.             uid = self._normalise_uid(raw_uid)
  513.             msg = self._dummy_msg(uid)
  514.             self.state.message_info_database.load_msg(msg)
  515.             if msg.c:
  516.                 continue
  517.             
  518.             
  519.             try:
  520.                 p.set_debuglevel(0)
  521.                 self.state.logFile.write('RETR MESSAGE\r\n')
  522.                 self.state.logFile.flush()
  523.                 (response, text_list, unused) = p.retr(msg_id)
  524.                 p.set_debuglevel(2)
  525.             except poplib.error_proto:
  526.                 e = None
  527.                 print >>sys.stderr, 'Cannot retrieve message', str(e)
  528.                 continue
  529.  
  530.             messageText = '\n'.join(text_list)
  531.             if uid is None:
  532.                 raw_uid = md5.md5(messageText).hexdigest()
  533.                 uid = self._normalise_uid(raw_uid)
  534.                 msg = self._dummy_msg(uid)
  535.                 self.state.message_info_database.load_msg(msg)
  536.                 if msg.c:
  537.                     continue
  538.                 
  539.             
  540.             if Options.options[('globals', 'verbose')]:
  541.                 print 'Downloading new message (id %s, uid %s)' % (msg_id, uid)
  542.             
  543.             msg_info = {
  544.                 'id': msg_id,
  545.                 'server_uid': raw_uid,
  546.                 'internal_id': uid,
  547.                 'length': len(messageText) }
  548.             self.processing_queue.put((messageText, uid, msg_info, self.current_account))
  549.         
  550.         
  551.         try:
  552.             p.quit()
  553.         except (poplib.error_proto, socket.error):
  554.             e = None
  555.             print >>sys.stderr, 'Error quitting:', str(e)
  556.  
  557.         self.remote_server = None
  558.         if Options.options[('globals', 'verbose')]:
  559.             print 'Retrieving messages ended.'
  560.         
  561.  
  562.     
  563.     def delete_messages(self, p):
  564.         messages_to_delete = self.state.delete_messages[self.current_account]
  565.         to_delete = copy.copy(messages_to_delete.items())
  566.         deleted = []
  567.         for uid, msginfo in to_delete:
  568.             if not msginfo:
  569.                 deleted.append(uid)
  570.                 continue
  571.             
  572.             old = self.state.delayed_messages[self.current_account]
  573.             
  574.             try:
  575.                 del old[uid]
  576.             except KeyError:
  577.                 pass
  578.  
  579.             self.state.delayed_messages[self.current_account] = old
  580.             self.state.delayed_messages.store()
  581.             msg = self._dummy_msg(uid)
  582.             self.state.message_info_database.load_msg(msg)
  583.             msg.block_state = REMOVED
  584.             self.state.message_info_database.store_msg(msg)
  585.             msg_id = self._find_message_ID(p, msginfo['id'], msginfo['server_uid'])
  586.             if msg_id == -1:
  587.                 if Options.options[('globals', 'verbose')]:
  588.                     print >>sys.stderr, "Couldn't find message to delete", msginfo['server_uid']
  589.                 
  590.                 deleted.append(uid)
  591.                 continue
  592.             
  593.             if Options.options[('globals', 'verbose')]:
  594.                 print 'Deleting message from server (id %s uid %s)' % (msg_id, uid)
  595.             
  596.             p.dele(msg_id)
  597.             deleted.append(uid)
  598.         
  599.         for uid in deleted:
  600.             del messages_to_delete[uid]
  601.         
  602.         self.state.delete_messages[self.current_account] = messages_to_delete
  603.         self.state.delete_messages.store()
  604.  
  605.     
  606.     def _find_message_ID(self, p, msg_id, correct_uid):
  607.         '''Find the message id that matches the given uid, which is
  608.         hopefully the value in id.
  609.         '''
  610.         if msg_id >= 0:
  611.             
  612.             try:
  613.                 (unused, unused, uid) = p.uidl(msg_id).split(' ', 3)
  614.             except poplib.error_proto:
  615.                 e = None
  616.                 
  617.                 try:
  618.                     (response, text_list, unused) = p.retr(msg_id)
  619.                 except poplib.error_proto:
  620.                     e = None
  621.                     uid = None
  622.  
  623.                 messageText = '\n'.join(text_list)
  624.                 uid = md5.md5(messageText).hexdigest()
  625.  
  626.             if uid == correct_uid:
  627.                 return msg_id
  628.             
  629.         
  630.         
  631.         try:
  632.             (response, msg_list, unused) = p.list()
  633.         except poplib.error_proto:
  634.             e = None
  635.             print >>sys.stderr, 'Cannot list messages:', str(e)
  636.             return -1
  637.  
  638.         for msg in msg_list:
  639.             (msg_id, unused) = msg.split()
  640.             
  641.             try:
  642.                 response = p.uidl(msg_id)
  643.             except poplib.error_proto:
  644.                 continue
  645.  
  646.             (unused, unused, uid) = response.split(' ', 3)
  647.             if uid == correct_uid:
  648.                 return msg_id
  649.                 continue
  650.         
  651.         for msg in msg_list:
  652.             (msg_id, unused) = msg.split()
  653.             
  654.             try:
  655.                 (response, text_list, unused) = p.retr(msg_id)
  656.             except poplib.error_proto:
  657.                 e = None
  658.                 uid = None
  659.  
  660.             messageText = '\n'.join(text_list)
  661.             uid = md5.md5(messageText).hexdigest()
  662.             if uid == correct_uid:
  663.                 return msg_id
  664.                 continue
  665.         
  666.         return -1
  667.  
  668.  
  669.  
  670. class bigger_producer(asynchat.simple_producer):
  671.     
  672.     def __init__(self, data, buffer_size = 8192):
  673.         asynchat.simple_producer.__init__(self, data, buffer_size)
  674.  
  675.  
  676.  
  677. class SEPOP3Server(Dibbler.BrighterAsyncChat, ProxyClassifier.ProxyClassifier, POPRetriever):
  678.     """Minimal POP3 server.  All messages are obtained by
  679.     downloading them from the 'real' server.
  680.     """
  681.     
  682.     def __init__(self, clientSocket, socketMap, serverName, serverPort, state, ssl = False):
  683.         self.isClosed = False
  684.         ProxyClassifier.ProxyClassifier.__init__(self)
  685.         Dibbler.BrighterAsyncChat.__init__(self, map = socketMap)
  686.         Dibbler.BrighterAsyncChat.set_socket(self, clientSocket, socketMap)
  687.         POPRetriever.__init__(self)
  688.         self.ac_out_buffer_size = 2 ** 15
  689.         self.ssl = ssl
  690.         if self.ssl:
  691.             self.pop_class = POP3_SSL
  692.         else:
  693.             self.pop_class = POP3
  694.         self.serverName = serverName
  695.         self.serverPort = serverPort
  696.         self.threads = []
  697.         self.creation_cache = { }
  698.         self.corpus = state.hamCorpus
  699.         self.waiting_corpus = state.waitingCorpus
  700.         self.unsure_corpus = state.unsureCorpus
  701.         self.state = state
  702.         self.mail_items = ()
  703.         self.state.proxies.append(self)
  704.         self.set_terminator('\r\n')
  705.         self.handlers = {
  706.             'QUIT': self.onQuit,
  707.             'CAPA': self.onCapa,
  708.             'STAT': self.onStat,
  709.             'LIST': self.onList,
  710.             'UIDL': self.onUidl,
  711.             'RETR': self.onRetr,
  712.             'TOP': self.onTop,
  713.             'USER': self.onUser,
  714.             'PASS': self.onPass,
  715.             'APOP': self.onApop,
  716.             'NOOP': self.onNoop,
  717.             'DELE': self.onDele,
  718.             'RSET': self.onRset }
  719.         
  720.         try:
  721.             self.remote_server = self.pop_class(self.state.logFile, self.serverName, self.serverPort)
  722.         except Exception:
  723.             e = None
  724.             self.push('-ERR %s' % (e,))
  725.             self.close_when_done()
  726.  
  727.         self.remote_server.set_debuglevel(2)
  728.         self.push(self.remote_server.getwelcome() + '\r\n')
  729.         self.request = ''
  730.         self.deleted_this_session = []
  731.  
  732.     
  733.     def push(self, data):
  734.         self.producer_fifo.push(bigger_producer(data, self.ac_out_buffer_size))
  735.         self.initiate_send()
  736.  
  737.     
  738.     def collect_incoming_data(self, data):
  739.         '''Asynchat override.'''
  740.         self.request = self.request + data
  741.  
  742.     
  743.     def found_terminator(self):
  744.         '''Asynchat override.'''
  745.         
  746.         try:
  747.             (command, args) = self.request.split(None, 1)
  748.         except ValueError:
  749.             command = self.request
  750.             args = ''
  751.  
  752.         command = command.upper()
  753.         handler = self.handlers.get(command, self.onUnknown)
  754.         self.push(handler(command, args))
  755.         self.request = ''
  756.  
  757.     
  758.     def close(self):
  759.         if not self.isClosed:
  760.             Dibbler.BrighterAsyncChat.close(self)
  761.             thread.start_new_thread(self._wait_for_threads, ())
  762.         
  763.         self.isClosed = True
  764.  
  765.     
  766.     def _wait_for_threads(self):
  767.         '''Let the state know that we are completely done once all the
  768.         threads are done.'''
  769.         if Options.options[('globals', 'verbose')]:
  770.             print 'Waiting for retrieve threads to complete for',
  771.             if hasattr(self, 'current_acount'):
  772.                 print current_account
  773.             else:
  774.                 print 'unknown account'
  775.         
  776.         for retrieve_thread in self.threads:
  777.             if retrieve_thread.isAlive():
  778.                 retrieve_thread.join()
  779.                 continue
  780.         
  781.         if Options.options[('globals', 'verbose')]:
  782.             print 'All retrieve threads complete for',
  783.             if hasattr(self, 'current_acount'):
  784.                 print current_account
  785.             else:
  786.                 print 'unknown account'
  787.         
  788.         self.threads = []
  789.         self.state.proxies.remove(self)
  790.         
  791.         try:
  792.             self.state.open_remote_connections.remove(self.current_account)
  793.         except (ValueError, AttributeError):
  794.             pass
  795.  
  796.         del self.socket
  797.  
  798.     
  799.     def close_when_done(self):
  800.         '''Asynchat override.'''
  801.         Dibbler.BrighterAsyncChat.close_when_done(self)
  802.         if not self.terminated:
  803.             self.expungeMessages()
  804.         
  805.  
  806.     
  807.     def onQuit(self, *unused):
  808.         self.push('+OK Goodbye!\r\n')
  809.         self.close_when_done()
  810.         return ''
  811.  
  812.     
  813.     def onCapa(self, *unused):
  814.         '''POP3 CAPA command.'''
  815.         return '\r\n'.join([
  816.             '+OK Capability list follows',
  817.             'TOP',
  818.             'USER',
  819.             'UIDL',
  820.             '.',
  821.             ''])
  822.  
  823.     
  824.     def onStat(self, *unused):
  825.         '''POP3 STAT command.'''
  826.         local_msgs = self._getSortedMessageList()
  827.         maildrop_size = []([ msginfo['length'] for msginfo in local_msgs ])
  828.         return '+OK %d %d\r\n' % (len(local_msgs), maildrop_size)
  829.  
  830.     
  831.     def _getSortedMessageList(self):
  832.         data = _[1]
  833.         data.sort()
  834.         return [ d[1] for d in data ]
  835.  
  836.     
  837.     def _creation_time(self, msg_id):
  838.         if msg_id in self.creation_cache:
  839.             return self.creation_cache[msg_id]
  840.         
  841.         msg = self.state.hamCorpus.get(msg_id)
  842.         if msg is None:
  843.             msg = self.state.waitingCorpus.get(msg_id)
  844.         
  845.         if msg is None:
  846.             msg = self.state.unsureCorpus.get(msg_id)
  847.         
  848.         if msg is None:
  849.             print >>sys.stderr, "Couldn't find message", msg_id
  850.             return time.time()
  851.         
  852.         stat = os.stat(msg.pathname())
  853.         creation_time = stat[9]
  854.         self.creation_cache[msg_id] = creation_time
  855.         return creation_time
  856.  
  857.     
  858.     def _getList(self, args, msginfo_att):
  859.         '''Implements the POP3 LIST and UIDL commands.'''
  860.         if Options.options[('globals', 'verbose')]:
  861.             start = time.time()
  862.         
  863.         if args:
  864.             
  865.             try:
  866.                 number = int(args)
  867.             except ValueError:
  868.                 number = -1
  869.  
  870.             if number < number:
  871.                 pass
  872.             elif number <= len(self.delayed_db):
  873.                 msginfo = self._getSortedMessageList()[number - 1]
  874.                 return '+OK %s\r\n' % msginfo[msginfo_att]
  875.             
  876.             return '-ERR no such message\r\n'
  877.         
  878.         return_lines = [
  879.             '+OK']
  880.         for i, msginfo in enumerate(self._getSortedMessageList()):
  881.             size = msginfo[msginfo_att]
  882.             return_lines.append('%d %s' % (i + 1, size))
  883.         
  884.         return_lines.append('.')
  885.         if Options.options[('globals', 'verbose')]:
  886.             print 'Getting list took', time.time() - start, 'seconds'
  887.         
  888.         return '\r\n'.join(return_lines) + '\r\n'
  889.  
  890.     
  891.     def onList(self, unused, args):
  892.         '''POP3 LIST command, with optional message number argument.'''
  893.         return self._getList(args, 'length')
  894.  
  895.     
  896.     def onUidl(self, unused, args):
  897.         '''POP3 UIDL command.'''
  898.         return self._getList(args, 'internal_id')
  899.  
  900.     
  901.     def _getMessage(self, number, maxLines):
  902.         '''Implements the POP3 RETR and TOP commands.'''
  903.         if number < number:
  904.             pass
  905.         elif number <= len(self.delayed_db):
  906.             msginfo = self._getSortedMessageList()[number - 1]
  907.             msg = self.corpus.get(msginfo['internal_id'])
  908.             if msg is None:
  909.                 msg = self.waiting_corpus.get(msginfo['internal_id'])
  910.                 if msg is None:
  911.                     msg = self.unsure_corpus[msginfo['internal_id']]
  912.                 
  913.             
  914.             msg.load()
  915.             messageText = msg.as_string()
  916.             
  917.             try:
  918.                 (headers, body) = re.split('\\n\\r?\\n', messageText, 1)
  919.             except ValueError:
  920.                 return '+OK %d octets\r\n%s\r\n.\r\n' % (len(messageText), messageText)
  921.  
  922.             if maxLines is None:
  923.                 body_lines = body.split('\n')
  924.             else:
  925.                 body_lines = body.split('\n')[:maxLines]
  926.             messageText = headers + '\r\n\r\n' + '\n'.join(body_lines)
  927.             return '+OK\r\n%s\r\n.\r\n' % messageText
  928.         
  929.         return '-ERR no such message\r\n'
  930.  
  931.     
  932.     def onRetr(self, unused, args):
  933.         '''POP3 RETR command.'''
  934.         
  935.         try:
  936.             number = int(args)
  937.         except ValueError:
  938.             number = -1
  939.  
  940.         return self._getMessage(number, None)
  941.  
  942.     
  943.     def onTop(self, unused, args):
  944.         '''POP3 RETR command.'''
  945.         
  946.         try:
  947.             (number, lines) = map(int, args.split())
  948.         except ValueError:
  949.             (number, lines) = (-1, -1)
  950.  
  951.         return self._getMessage(number, lines)
  952.  
  953.     
  954.     def _setupAccount(self):
  955.         """Set the 'current_account' attribute, and create a new
  956.         account for this user/server combination if necessary."""
  957.         self.current_account = '%s_%s_POP' % (self.userName, self.serverName)
  958.         if self.current_account not in self.state.blocked_messages:
  959.             self.state.blocked_messages[self.current_account] = { }
  960.             self.state.blocked_messages.store()
  961.         
  962.         if self.current_account not in self.state.delayed_messages:
  963.             self.state.delayed_messages[self.current_account] = { }
  964.             self.state.delayed_messages.store()
  965.             m_f_s = email.message_from_string
  966.             welcomeText = self.get_blocking_welcome_message(self.userName, self.serverName)
  967.             msg = m_f_s(welcomeText, _class = message.SEHeaderMessage)
  968.             msg.setId(self.state.getNewMessageName())
  969.             corpus_msg = self.state.waitingCorpus.makeMessage(msg.getId(), msg.as_string())
  970.             self.state.waitingCorpus.addMessage(corpus_msg, observer_flags = NO_TRAINING_FLAG)
  971.             msg_id = msg.getId()
  972.             msg_info = {
  973.                 'server_uid': -1,
  974.                 'id': -1,
  975.                 'internal_id': msg_id,
  976.                 'length': len(welcomeText) }
  977.             self.state.delayed_messages.addMessage(self.current_account, msg_id, msg_info)
  978.         
  979.         if self.current_account not in self.state.delete_messages:
  980.             self.state.delete_messages[self.current_account] = { }
  981.             self.state.delete_messages.store()
  982.         
  983.         self.mail_items = tuple(self.delayed_db.items())
  984.  
  985.     
  986.     def onUser(self, unused, args):
  987.         '''POP3 USER command.'''
  988.         if self.remote_server is None:
  989.             return '-ERR Already authenticated'
  990.         
  991.         
  992.         try:
  993.             response = self.remote_server.user(args)
  994.         except (poplib.error_proto, socket.sslerror):
  995.             e = None
  996.             return str(e) + '\r\n'
  997.  
  998.         self.userName = args
  999.         self._setupAccount()
  1000.         return response + '\r\n'
  1001.  
  1002.     
  1003.     def onPass(self, unused, args):
  1004.         '''POP3 PASS command.'''
  1005.         if self.current_account in self.state.open_remote_connections:
  1006.             if Options.options[('globals', 'verbose')]:
  1007.                 print 'Connection already open to', self.current_account, '- refusing new connection.'
  1008.             
  1009.             self.state.open_remote_connections.append(self.current_account)
  1010.             self.push('-ERR Mailbox is locked by another process.  Another mail client is using this mailbox.  Please try again in a few minutes.\r\n')
  1011.             self.close_when_done()
  1012.             return ''
  1013.         
  1014.         self.state.open_remote_connections.append(self.current_account)
  1015.         if self.remote_server is None:
  1016.             self.push('-ERR Already authenticated')
  1017.             self.close_when_done()
  1018.             return ''
  1019.         
  1020.         
  1021.         try:
  1022.             response = self.remote_server.pass_(args)
  1023.         except poplib.error_proto:
  1024.             e = None
  1025.             self.push(str(e) + '\r\n')
  1026.             self.close_when_done()
  1027.             return ''
  1028.  
  1029.         self.password = args
  1030.         connection = (self.serverName, self.serverPort, self.userName, self.password, 'pop3', self.ssl)
  1031.         self.state.model_notifier.add_connection(connection)
  1032.         self.use_apop = False
  1033.         retrieve_thread = threading.Thread(target = self.retrieveMessages)
  1034.         retrieve_thread.setDaemon(True)
  1035.         self.threads.append(retrieve_thread)
  1036.         retrieve_thread.start()
  1037.         return response + '\r\n'
  1038.  
  1039.     
  1040.     def onApop(self, unused, args):
  1041.         '''POP3 APOP command.'''
  1042.         if self.remote_server is None:
  1043.             return '-ERR Already authenticated'
  1044.         
  1045.         
  1046.         try:
  1047.             response = self.remote_server.apop(*args)
  1048.         except poplib.error_proto:
  1049.             e = None
  1050.             return str(e) + '\r\n'
  1051.  
  1052.         (self.userName, self.password) = args.split()
  1053.         self.use_apop = True
  1054.         self._setupAccount()
  1055.         retrieve_thread = threading.Thread(target = self.retrieveMessages)
  1056.         retrieve_thread.setDaemon(True)
  1057.         self.threads.append(retrieve_thread)
  1058.         retrieve_thread.start()
  1059.         return response + '\r\n'
  1060.  
  1061.     
  1062.     def onNoop(self, *unused):
  1063.         '''POP3 NOOP command.'''
  1064.         return '+OK NOOP successfull\r\n'
  1065.  
  1066.     
  1067.     def onDele(self, unused, args):
  1068.         '''POP3 DELE command.'''
  1069.         
  1070.         try:
  1071.             number = int(args)
  1072.         except ValueError:
  1073.             number = -1
  1074.  
  1075.         if number < number:
  1076.             pass
  1077.         elif number <= len(self.delayed_db):
  1078.             msginfo = self._getSortedMessageList()[number - 1]
  1079.             key = msginfo['internal_id']
  1080.             old = self.messages_to_delete
  1081.             old[key] = msginfo
  1082.             self.messages_to_delete = old
  1083.             self.deleted_this_session.append(key)
  1084.             return '+OK message deleted\r\n'
  1085.         
  1086.         return '-ERR no such message\r\n'
  1087.  
  1088.     
  1089.     def onRset(self, *unused):
  1090.         '''POP3 RSET command.'''
  1091.         for key in self.deleted_this_session:
  1092.             old = self.messages_to_delete
  1093.             del old[key]
  1094.             self.messages_to_delete = old
  1095.         
  1096.         self.deleted_this_session = []
  1097.         return '+OK\r\n'
  1098.  
  1099.     
  1100.     def onUnknown(self, command, unused):
  1101.         '''Unknown POP3 command.'''
  1102.         return '-ERR Unknown command: %s\r\n' % repr(command)
  1103.  
  1104.     
  1105.     def expungeMessages(self):
  1106.         '''Delete mail that the user has asked to be deleted.'''
  1107.         
  1108.         try:
  1109.             p = self.pop_class(self.state.logFile, self.serverName, self.serverPort)
  1110.         except Exception:
  1111.             e = None
  1112.             print >>sys.stderr, "Can't connect to delete:", str(e)
  1113.             return None
  1114.  
  1115.         if Options.options[('globals', 'verbose')]:
  1116.             p.set_debuglevel(2)
  1117.         
  1118.         if not hasattr(self, 'use_apop'):
  1119.             return None
  1120.         
  1121.         if self.use_apop:
  1122.             p.apop(self.userName, self.password)
  1123.         else:
  1124.             
  1125.             try:
  1126.                 p.user(self.userName)
  1127.             except poplib.error_proto:
  1128.                 e = None
  1129.                 print >>sys.stderr, "Can't authenticate:", str(e)
  1130.                 return None
  1131.  
  1132.             
  1133.             try:
  1134.                 p.pass_(self.password)
  1135.             except poplib.error_proto:
  1136.                 e = None
  1137.                 print >>sys.stderr, "Can't authenticate:", str(e)
  1138.                 return None
  1139.  
  1140.         self.delete_messages(p)
  1141.         
  1142.         try:
  1143.             p.quit()
  1144.         except poplib.error_proto:
  1145.             e = None
  1146.             print >>sys.stderr, 'Error quitting:', str(e)
  1147.  
  1148.  
  1149.  
  1150.